Skip to content

fix(components/docs-tools): links in if statements are correctly registered in table of contents#4339

Draft
Blackbaud-ErikaMcVey wants to merge 1 commit intomainfrom
docs-tools-fix
Draft

fix(components/docs-tools): links in if statements are correctly registered in table of contents#4339
Blackbaud-ErikaMcVey wants to merge 1 commit intomainfrom
docs-tools-fix

Conversation

@Blackbaud-ErikaMcVey
Copy link
Copy Markdown
Contributor

@Blackbaud-ErikaMcVey Blackbaud-ErikaMcVey commented Mar 25, 2026

Claude's explanation of the issue:

Problem: SkyDocsHeadingAnchorService.#getLinks() used document.querySelectorAll('sky-docs-heading-anchor') to find and sort registered anchors by DOM position. When heading anchors were rendered inside structural directives like @if, Angular's ngAfterViewInit fired (triggering register) before the elements were attached to the document. Since querySelectorAll couldn't find those elements, they were silently dropped from the emitted links array, causing the table of contents to appear empty.

Fix: Instead of querying the DOM, the service now stores each anchor's native element reference in a Map during register(). The #getLinks() method sorts using compareDocumentPosition on those stored references, ensuring all registered anchors are always included regardless of their attachment timing. A secondary splice bug (missing the deleteCount argument, causing it to remove all anchors after the target) was also fixed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved heading anchor link generation stability and accuracy.
    • Fixed anchor ordering to correctly reflect their position in the document structure.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 25, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 81c1ff17-d05d-468c-a668-768a27bf83e5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs-tools-fix

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Mar 25, 2026

View your CI Pipeline Execution ↗ for commit 51fc32c

Command Status Duration Result
nx build code-examples-playground --baseHref=ht... ✅ Succeeded 1m 49s View ↗
nx build playground --baseHref=https://blackbau... ✅ Succeeded 1m 40s View ↗
nx build integration --baseHref=https://blackba... ✅ Succeeded 28s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-25 18:31:23 UTC

@blackbaud-sky-build-user
Copy link
Copy Markdown
Collaborator

Storybook preview

Component Storybooks:

  • (no component storybooks affected in this pr)

Apps:

// rather than the order at which they were registered.
for (const el of els) {
this.#anchors.forEach((anchor) => {
if (anchor.equals(el)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This public method is no longer used on the anchor component; can you remove it?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an issue in @skyux/docs-tools where heading anchors rendered inside structural directives (e.g., @if) could fail to appear in the table of contents due to DOM-query-based sorting during registration.

Changes:

  • Update SkyDocsHeadingAnchorService to track each registered anchor’s native element in a Map and sort anchors via compareDocumentPosition instead of document.querySelectorAll(...).
  • Fix unregister() to remove only the target anchor (splice(index, 1)).
  • Update SkyDocsHeadingAnchorComponent to pass its native element into the service during registration.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
libs/components/docs-tools/src/lib/modules/heading-anchor/heading-anchor.service.ts Stores element refs for registered anchors, sorts links by element document position, and fixes unregister removal.
libs/components/docs-tools/src/lib/modules/heading-anchor/heading-anchor.component.ts Passes the host element to the service during registration.

Comment on lines 29 to 33
if (!this.#anchors.includes(anchor)) {
this.#anchors.push(anchor);
this.#anchorElements.set(anchor, element);
this.#anchorsChange.next(this.#getLinks());
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

register() emits anchorsChange synchronously. In the reported @if timing scenario the host element may still be disconnected from document, so #getLinks() may compute an implementation-specific order (and never re-emit once the element is attached). Consider deferring the emission (e.g., queueing a microtask / next tick) or otherwise scheduling a second recomputation after the element is connected so the TOC order reliably reflects actual DOM order.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +66
const position = elA.compareDocumentPosition(elB);

if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
return -1;
}

if (position & Node.DOCUMENT_POSITION_PRECEDING) {
return 1;
}

return 0;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sort comparator uses compareDocumentPosition(), but it doesn't account for Node.DOCUMENT_POSITION_DISCONNECTED. For disconnected nodes browsers may also set PRECEDING/FOLLOWING in an implementation-specific way, which can produce a stable but incorrect DOM order. Consider explicitly checking for DISCONNECTED and returning 0 (or another deterministic fallback) until both nodes share a document/root.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
libs/components/docs-tools/src/lib/modules/heading-anchor/heading-anchor.service.ts (1)

25-34: Consider adding a guard for SSR compatibility.

The compareDocumentPosition DOM API is only available in browser contexts. While this docs-tools module likely only runs in the browser, if there's any chance of SSR usage, a platform check could prevent runtime errors.

🛡️ Optional: Add platform check if SSR is ever a concern
+import { Injectable, OnDestroy, PLATFORM_ID, inject } from '@angular/core';
+import { isPlatformBrowser } from '@angular/common';
 
 `@Injectable`()
 export class SkyDocsHeadingAnchorService implements OnDestroy {
+  readonly `#platformId` = inject(PLATFORM_ID);
   `#anchors`: SkyDocsHeadingAnchorComponent[] = [];
   `#anchorElements` = new Map<SkyDocsHeadingAnchorComponent, Element>();

Then in register():

public register(anchor: SkyDocsHeadingAnchorComponent, element: Element): void {
  if (!isPlatformBrowser(this.#platformId)) {
    return;
  }
  // ... existing logic
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/components/docs-tools/src/lib/modules/heading-anchor/heading-anchor.service.ts`
around lines 25 - 34, The register method should guard against server-side
execution to avoid using DOM-only APIs; add a platform check at the start of
register (e.g., use isPlatformBrowser(this.#platformId)) and return early when
not in a browser before touching `#anchors`, `#anchorElements`, or calling
`#anchorsChange.next`; ensure the service has access to the injected platform id
(this.#platformId) and import isPlatformBrowser so the method short-circuits in
SSR environments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@libs/components/docs-tools/src/lib/modules/heading-anchor/heading-anchor.service.ts`:
- Around line 25-34: The register method should guard against server-side
execution to avoid using DOM-only APIs; add a platform check at the start of
register (e.g., use isPlatformBrowser(this.#platformId)) and return early when
not in a browser before touching `#anchors`, `#anchorElements`, or calling
`#anchorsChange.next`; ensure the service has access to the injected platform id
(this.#platformId) and import isPlatformBrowser so the method short-circuits in
SSR environments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ee3b8297-55b6-4a85-85e1-8e2533a46184

📥 Commits

Reviewing files that changed from the base of the PR and between 2d2776f and 51fc32c.

📒 Files selected for processing (2)
  • libs/components/docs-tools/src/lib/modules/heading-anchor/heading-anchor.component.ts
  • libs/components/docs-tools/src/lib/modules/heading-anchor/heading-anchor.service.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants